import threading

# noinspection PyPep8
import time
from collections import deque

import serial
from PyQt5.QtCore import QObject, QThread, QWaitCondition, QMutex, pyqtSignal
from loguru import logger
from serial import SerialException

from Config import Config
from singleton import singleton


@singleton
class DiyArmInst(QObject):
    def __init__(self):
        super().__init__()

        self.arms = {}

        conf_inst = Config().inst()
        port = conf_inst.get("DiyArm1", "port")
        self.arms["DiyArm1"] = DiyArm(com_port=port)
        # print(self.arms["DiyArm1"])

    def add_cmd(self, name, cmd, action_name, task_id=-1):
        arm = self.arms.get(name)
        # logger.debug(task_id)/
        if arm:
            arm.add_cmd(cmd, action_name, task_id)

    def close(self):
        for name, arm in self.arms.items():
            arm.shutdown()

    def event(self, name):
        arm = self.arms.get(name)
        if arm:
            return arm.taskfinished

    def is_running(self, name):
        arm = self.arms.get(name)
        if arm:
            return arm.is_running()


class DiyArm(QThread):
    # def __init__(self, *args, **kwargs):
    taskfinished = pyqtSignal(tuple)

    # noinspection PyPep8,PyPep8,PyPep8
    def __init__(self, com_port=None, baud_rate=115200,
                 arduino_instance_id=1, arduino_wait=4,
                 sleep_tune=0.000001,
                 shutdown_on_exception=True):
        super(DiyArm, self).__init__()
        self.start_time = time.time()

        self.com_port = com_port
        self.baud_rate = baud_rate
        self.arduino_instance_id = arduino_instance_id
        self.arduino_wait = arduino_wait
        self.sleep_tune = sleep_tune
        self.shutdown_on_exception = shutdown_on_exception

        # initialize threading parent
        # threading.Thread.__init__(self)
        self.daemon = True
        self._is_running = False
        # serial port in use
        self.serial_port = None
        self._manual_open()

        self.shutdown_flag = False

        self.the_deque = deque()
        self.the_name_deque = deque()
        self.the_task_id_deque = deque()
        # self.deque_lock = threading.Lock()

        self.cond = QWaitCondition()
        self.mutex = QMutex()

        self.start()

    def run(self):
        while not self.shutdown_flag:
            self.mutex.lock()
            # logger.debug("lock")
            self.cond.wait(self.mutex)
            # logger.debug("wait")
            self.mutex.unlock()

            while self.the_deque:
                self.mutex.lock()
                cmds = self.the_deque.popleft()
                name = self.the_name_deque.popleft()
                task_id = self.the_task_id_deque.popleft()

                # logger.debug(task_id)
                # logger.debug("cmds len = {}".format(len(cmds)))
                for c in cmds:
                    try:
                        self.serial_port.flushInput()
                        # logger.debug(1)
                        result = self.serial_port.write(c.encode("utf-8"))
                        # logger.debug(2)
                        result = self.serial_port.readline()
                        # logger.debug(result)

                        # time.sleep(0.1)

                        # pass
                    except serial.SerialTimeoutException:
                        logger.debug("SerialTimeoutException")
                    except serial.SerialException:
                        if self.shutdown_on_exception:
                            self.shutdown()
                        raise RuntimeError('write fail in _send_command')
                # logger.debug(name)
                # logger.debug(task_id)
                self.taskfinished.emit((name, task_id))
                self._is_running = False
                self.mutex.unlock()
                # if self.shutdown_flag: break

            # return result
            # time.sleep(0.1)

    def _manual_open(self):
        """
        Com port was specified by the user - try to open up that port

        """
        # if port is not found, a serial exception will be thrown
        try:
            print(f'Opening {self.com_port}...')
            # self.arduinoSerial = serial.Serial(items[0].text(), int(baud), timeout=3)
            print(self.com_port, self.baud_rate)
            self.serial_port = serial.Serial(self.com_port, self.baud_rate, timeout=5)

            print(f'\nWaiting {self.arduino_wait} seconds(arduino_wait) for Arduino devices to '
                  'reset...')
            # time.sleep(self.arduino_wait)
            # time.sleep(self.arduino_wait)
        except KeyboardInterrupt:
            raise RuntimeError('User Hit Control-C')
        except Exception as e:
            print(e)

    def shutdown(self):
        """
        This method attempts an orderly shutdown
        If any exceptions are thrown, they are ignored.

        """

        self.shutdown_flag = True
        # self.join()

        # self._stop_threads()

        try:
            # stop all reporting - both analog and digital
            # for pin in range(len(self.analog_pins)):
            #     self.disable_analog_reporting(pin)
            #
            # for pin in range(len(self.digital_pins)):
            #     self.disable_digital_reporting(pin)
            # self.send_reset()

            self.serial_port.reset_input_buffer()
            self.serial_port.close()
        except (RuntimeError, SerialException, OSError):
            # ignore error on shutdown
            pass

    def add_cmds(self, cmds):
        with self.mutex:
            self.the_deque.extend(cmds)

    def add_cmd(self, cmd, name="", task_id=0):

        self.mutex.lock()
        # if cmd.find("\r") < 0:
        #     cmd = cmd + "\r"
        # logger.debug(cmd, name, task_id)
        self.the_deque.append(cmd)
        self.the_name_deque.append(name)
        self.the_task_id_deque.append(task_id)
        self._is_running = True
        self.mutex.unlock()
        self.cond.wakeOne()

    def is_running(self):
        return self._is_running
